﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceProcess;
using System.Threading;
using System.Timers;
using Kobets.Automation.Chimera.Common;
using Ionic.Zip;
using Kobets.Automation.Chimera.Common.Contracts;
using Kobets.Automation.Chimera.Common.Models;
using Kobets.Automation.Chimera.Controller.Properties;
using Environment = Kobets.Automation.Chimera.Common.Models.Environment;
using Timer = System.Timers.Timer;


namespace Kobets.Automation.Chimera.Controller
{
    public partial class ChimeraController : ServiceBase
    {
        #region Fields

        private static ServiceHost _s_host;
        private const string SourceName = "Chimera Controller";
        private static readonly Dictionary<string, ChannelFactory<IAgentContract>> _s_agentsChannels = new Dictionary<string, ChannelFactory<IAgentContract>>();
        private static readonly Timer _s_timer = new Timer();

        #endregion

        public static Enums.ControllerStatus ControllerStatus;
        public static EventLog MyEventLog = new EventLog();
        public static ObservableUniqueCollection<Agent> Agents = new ObservableUniqueCollection<Agent>();
        public static ObservableQueue<KeyValuePair<Recipe, Environment>> WorkloadQueue = new ObservableQueue<KeyValuePair<Recipe, Environment>>();

        public ChimeraController()
        {
            InitializeComponent();
            ControllerStatus = Enums.ControllerStatus.Idle;
        }

        protected override void OnStart(string[] args)
        {
            _s_timer.Interval = 60000D;
            _s_timer.Elapsed += TimerElapsed;
            _s_timer.Enabled = true;

            if (!EventLog.SourceExists(SourceName))
                EventLog.CreateEventSource(SourceName, "Application");

            MyEventLog.Source = SourceName;
            MyEventLog.WriteEntry("Service started");

            Agents.CollectionChanged += AgentsCollectionChanged;

            if (!String.IsNullOrWhiteSpace(Settings.Default.Agents))
                Agents.AddRange(DeserializeAgents(Settings.Default.Agents));

            var baseAddress = new Uri("http://" + CommonMethods.GetLocalhostFqdn() + ":8081/chimeracontroller");
            _s_host = new ServiceHost(typeof(ControllerContract), baseAddress);
            var smb = new ServiceMetadataBehavior { HttpGetEnabled = true, MetadataExporter = { PolicyVersion = PolicyVersion.Policy15 } };
            _s_host.Description.Behaviors.Add(smb);
            _s_host.Open();

            MyEventLog.WriteEntry("Comm channel open on " + baseAddress);
        }

        protected override void OnStop()
        {
            OnStopInternal();
        }

        #region Public Methods

        public static void Restart()
        {
            OnStopInternal();
            System.Environment.Exit(1);
        }

        public static void RefreshAgentsStatus(Agent[] agents = null)
        {
            if (agents == null)
            {
                foreach (var agent in Agents)
                    agent.Status = GetAgentStatus(agent);
            }
            else
            {
                foreach (var curAgent in agents.Select(agent => Agents.First(a => a.Name == agent.Name)))
                    curAgent.Status = GetAgentStatus(curAgent);
            }
        }

        public static void StopExecution(Guid environmentid)
        {
            // todo: fix parsing for env nodes

            //foreach (var agent in Agents.Where(a => a.Status == Enums.AgentStatus.Running))
            //{
            //    StopAgentExecution(agent);
            //    Thread.Sleep(1000);
            //}
        }

        public static void ExecuteById(Guid recipeId, Guid environmentid)
        {
            using (var db = new ChimeraContext())
            {
                var recipe = db.Recipes.Single(r => r.Id == recipeId);
                var env = db.Environments.Single(e => e.Id == environmentid);
                WorkloadQueue.Enqueue(new KeyValuePair<Recipe, Environment>(recipe, env));
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// This is where the magic happens
        /// </summary>
        /// <param name="recipe"></param>
        /// <param name="environment"></param>
        private static void ExecuteWorkload(Recipe recipe, Environment environment)
        {
            if (recipe == null) throw new ArgumentNullException("recipe");
            if (environment == null) throw new ArgumentNullException("environment");
            ControllerStatus = Enums.ControllerStatus.Busy;

            try
            {
                // check nodes are registered agents and idle
                var nodes = environment.Nodes.Select(s => s.NetworkName);

                foreach (var node in nodes.Where(n => CommonMethods.IsHostAlive(n) && Agents.Any(a => a.NetworkName == n)))
                {
                    throw new Exception("Node " + node + " is not answering or not a registered agent.");
                }

                // parse recipe
                var parsedSteps = new Dictionary<Node, List<Step>>();
                //  - match steps to nodes
                foreach (var step in recipe.Steps)
                {
                    // IMPORTANT! Assume each env has only 1 role of each type
                    var node = environment.Nodes.First(n => n.Role == step.ExecuteOn);

                    if (!parsedSteps.ContainsKey(node))
                    {
                        parsedSteps.Add(node, new List<Step>().OrderBy(o => o.OrderId).ToList());
                    }

                    parsedSteps[node].Add(step);
                }

                // add jobs for all steps with status Pending
                using (var db = new ChimeraContext())
                {
                    foreach (var pStep in parsedSteps)
                    {
                        foreach (var step in pStep.Value)
                        {
                            db.Jobs.Add(new Job
                            {
                                Id = Guid.NewGuid(),
                                CreatedOn = DateTime.Now,
                                Name = step.Name,
                                Desc = step.Desc,
                                Node = pStep.Key,
                                Status = CommonMethods.GetJobStatus(Enums.JobStatus.Pending),
                                Type = step.JobType,
                                StepId = step.Id
                            });
                            db.SaveChanges();
                        }
                    }
                }

                // send steps to nodes
                foreach (var kvp in parsedSteps)
                {
                    var channelFactory = _s_agentsChannels[kvp.Key.NetworkName];
                    if (channelFactory.State != CommunicationState.Opened) channelFactory.Open();
                    var client = channelFactory.CreateChannel();
                    client.RunSteps(kvp.Value);
                    ((IClientChannel)client).Close();
                }
            }
            catch (Exception ex)
            {
                MyEventLog.WriteEntry(ex.Message, EventLogEntryType.Error);
            }
            finally
            {
                ControllerStatus = Enums.ControllerStatus.Idle;
            }
        }

        private static void OnStopInternal()
        {
            _s_timer.Enabled = false;
            if (_s_host != null && _s_host.State == CommunicationState.Opened) _s_host.Close();
            Settings.Default.Agents = SerializeAgents(Agents);
            Settings.Default.Save();
            MyEventLog.WriteEntry("Service stopped");
        }

        /// <summary>
        /// Called when agents registers/unregisters itself with the controller
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void AgentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            try
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        var newAgent = (Agent)e.NewItems[0];
                        var identity = EndpointIdentity.CreateDnsIdentity("Chimera");
                        var address = new EndpointAddress(new Uri("http://" + newAgent.NetworkName + ":8080/chimeraagent"), identity);
                        var channel = new ChannelFactory<IAgentContract>(new CustomHttpBinding(false) { SendTimeout = TimeSpan.Parse("00:10:00"), ReceiveTimeout = TimeSpan.Parse("00:10:00"), MaxMessageSize = 52428800 }, address);
                        channel.Credentials.UserName.UserName = "chimera";
                        channel.Credentials.UserName.Password = "chimera";
                        channel.Credentials.ClientCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByIssuerName, "Chimera");
                        _s_agentsChannels.Add(newAgent.Name, channel);
                        newAgent.Status = GetAgentStatus(newAgent);
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        var oldAgent = (Agent)e.OldItems[0];
                        if (_s_agentsChannels.ContainsKey(oldAgent.Name))
                        {
                            if (_s_agentsChannels[oldAgent.Name].State == CommunicationState.Opened)
                                _s_agentsChannels[oldAgent.Name].Close();
                            _s_agentsChannels.Remove(oldAgent.Name);
                        }
                        break;
                }

                Settings.Default.Agents = SerializeAgents(Agents);
                Settings.Default.Save();
            }
            catch (Exception ex)
            {
                MyEventLog.WriteEntry(ex.Message);
            }
        }

        private static Enums.AgentStatus GetAgentStatus(Agent agent)
        {
            if (agent == null) throw new ArgumentNullException("agent");

            var channelFactory = _s_agentsChannels[agent.Name];
            if (channelFactory.State != CommunicationState.Opened) channelFactory.Open();

            if (channelFactory.State != CommunicationState.Opened)
                return Enums.AgentStatus.Disconnected;

            var client = channelFactory.CreateChannel();
            var status = Enums.AgentStatus.Disconnected;

            try
            {
                if (channelFactory.State == CommunicationState.Opened)
                {
                    status = client.GetStatus();
                    ((IClientChannel)client).Close();
                }
            }
            catch (Exception ex) { }

            return status;
        }

        private static void StopAgentExecution(Agent agent)
        {
            if (agent == null) throw new ArgumentNullException("agent");

            try
            {
                var channelFactory = _s_agentsChannels[agent.Name];
                if (channelFactory.State != CommunicationState.Opened) channelFactory.Open();
                var client = channelFactory.CreateChannel();
                client.StopExecution();
                ((IClientChannel)client).Close();
                Debug.WriteLine("Stopping execution on: " + agent.Name);
            }
            catch (Exception ex)
            {
                MyEventLog.WriteEntry("StopAgentExecution: " + ex.Message, EventLogEntryType.Error);
            }
        }

        /// <summary>
        /// Runs every minute, used for monitoring and execution of pending work
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void TimerElapsed(object sender, ElapsedEventArgs e)
        {
            // refresh agents status
            foreach (var agent in Agents)
                agent.Status = GetAgentStatus(agent);

            if (ControllerStatus != Enums.ControllerStatus.Idle) return;

            // if there are more tasks remaining, execute the next one
            while (WorkloadQueue.Count > 0)
            {
                var currItem = WorkloadQueue.Dequeue();
                ExecuteWorkload(currItem.Key, currItem.Value);
            }

            GC.Collect();
        }

        private static string SerializeAgents(IEnumerable<Agent> agents)
        {
            if (agents == null) throw new ArgumentNullException("agents");

            var sagents = agents.Select(agent => new SerializableAgent
                                                     {
                                                         Name = agent.Name,
                                                         NetworkName = agent.NetworkName,
                                                         Status = agent.Status,
                                                         Controller = agent.Controller
                                                     }).ToList();

            using (var ms = new MemoryStream())
            {
                using (new StreamReader(ms))
                {
                    var bf = new BinaryFormatter();
                    bf.Serialize(ms, sagents);
                    ms.Position = 0;
                    var buffer = new byte[(int)ms.Length];
                    ms.Read(buffer, 0, buffer.Length);
                    return Convert.ToBase64String(buffer);
                }
            }
        }

        private static IEnumerable<Agent> DeserializeAgents(string serializedAgents)
        {
            if (serializedAgents == null) throw new ArgumentNullException("serializedAgents");
            var output = new ObservableUniqueCollection<Agent>();

            using (var ms = new MemoryStream(Convert.FromBase64String(serializedAgents)))
            {
                var bf = new BinaryFormatter();
                var sagents = (List<SerializableAgent>)bf.Deserialize(ms);

                output.AddRange(sagents.Select(serializableAgent => new Agent
                                                                        {
                                                                            Name = serializableAgent.Name,
                                                                            NetworkName = serializableAgent.NetworkName,
                                                                            Status = serializableAgent.Status,
                                                                            Controller = serializableAgent.Controller
                                                                        }));
            }

            return output;
        }

        #endregion
    }
}
